JVM物理结构:
Java编译器只面向JVM,生成JVM能理解的代码或字节码文件,Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行!
Java代码编译和执行的整个过程包含三个重要的机制:
- Java源码编译机制
- 类加载机制
- 类执行机制
Java源码编译机制由三个过程组成:
- 分析和输入到符号表
- 注解处理
- 语义分析和生成class文件
类加载过程中会先检查类是否已被加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个ClassLoader已加载就视为已加载该类,保证此类只被加载一次,而加载的顺序是自顶向下的,也就是由上层来逐层尝试加载此类。
JVM是基于栈的体系结构来执行class字节码文件的,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是由局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
其中,Java堆和方法区是线程共享内存区,而虚拟机栈、本地方法栈和程序计数器则是线程私有内存区。
程序计数器
每条线程都有一个独立的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。当程序执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法时,该计数器的值为空。另外,该内存区是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。
Java虚拟机栈
该区域是线程私有的,它的生命周期和线程相同
本地方法栈
虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统方法服务。
Java堆
Java堆是所有线程共享的一块内存区域,几乎所有的对象实例和数组都在这类分配内存,Java Heap是垃圾收集器管理的主要区域,因此很多时候称为GC堆。
方法区
方法区是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区又被称为永久代,但这仅仅对于Sun HotSpot来说。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,另外,虚拟机规范允许该区域可以选择不实现垃圾回收,相对而言,垃圾收集行为在这个区域比较少出现,该区域的内存回收目标主要是针对废弃常量和无用类的回收。运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等信息之外,还有一项就是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另一个重要特性就是具备动态性,Java语言并不要求常量一定只能在编译器产生,也就是并非预置于Class文件中的常量池中的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用较多的是String类的intern()方法。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受 Java 堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致 OutOfMemoryError 异常出现。在 JDK1.4 中新引入了 NIO 机制,它是一种基于通道与缓冲区的新 I/O 方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
类初始化是类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的Java程序代码,虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指定时,如果类还没有进行初始化,则需要触发其初始化,生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)以及调用一个类的静态方法时;
- 使用java.lang.refect包的方法对类进行反射调用时,如果类还没有进行初始化,则需要触发其初始化;
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。
虚拟机只有这四种情况才会触发类的初始化,称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用,下面举一些例子来说明被动引用:
通过子类引用父类中的静态字段,这时对子类的引用为被动引用,因此不会初始化子类,只会初始化父类:
123456789101112131415161718class Father {public static int m = 33;static {System.out.println("父类被初始化");}}class Child extends Father {static {System.out.println("子类被初始化");}}public class StaticTest {public static void main(String[] args) {System.out.println(Child.m);}}执行结果如下:
12父类被初始化33对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
常量在编译阶段会被存入调用它的类的常量池中,本质上没有直接引用到定义该常量的类,因此不会触发定义常量的类的初始化:
123456789101112class Const {public static final String NAME = "我是常量";static {System.out.println("初始化Const类");}}public class FinalTest{public static void main(String[] args){System.out.println(Const.NAME);}}执行后输出的结果为:
1我是常量虽然程序中引用了 const 类的常量 NAME,但是在编译阶段将此常量的值“我是常量”存储到了调用它的类 FinalTest 的常量池中,对常量 Const.NAME 的引用实际上转化为了 FinalTest 类对自身常量池的引用。也就是说,实际上 FinalTest 的 Class 文件之中并没有 Const 类的符号引用入口,这两个类在编译成 Class 文件后就不存在任何联系了。
通过数组定义来引用类,不会触发类的初始化:
1234567891011class Const{static{System.out.println("初始化Const类");}}public class ArrayTest{public static void main(String[] args){Const[] con = new Const[5];}}执行后不输出任何信息,说明 Const 类并没有被初始化。
Java程序最初是仅仅通过解释器进行执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其是当某个方法或代码块运行的特别频繁的时候,这种方式的执行效率就显得很低,于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块执行比较频繁的时候,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。
当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行后,随着时间的推移,编译器会逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获得更高的执行效率。
解释执行可以节约内存,而编译执行可以提升效率。
运行过程中会被即时编译器编译的“热点代码”有两类:
- 被多次调用的方法
- 被多次调用的循环体
本文参考自深入理解 Java 虚拟机。